package ua.edu.nuos.jeetraining2012.cms.util.google.api.services.drive;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.googleapis.media.MediaHttpUploader;
import com.google.api.client.http.FileContent;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.Permission;
import org.slf4j.Logger;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.GoogleAuthorizer;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.predefined.StandardGoogleApiVariables;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.auth.GoogleAuthException;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.drive.GDriveFilePermissionException;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.drive.GDriveUploadException;
import ua.edu.nuos.jeetraining2012.cms.util.google.api.service.exception.transport.GoogleTransportException;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * GoogleDriveUploader.
 * <p/>
 * Service class that upload files to google drive account.
 *
 * @author Victor N. Ganichev victor.ganichev@gmail.com
 * @version 1.0 Date: 08.06.13 Time: 23:36
 * @since 1.0
 */
public class GoogleDriveUploader {

    /** Define application name */
    final private static String APPLICATION_NAME = StandardGoogleApiVariables.APPLICATION_NAME;

    /** Define catalog with uploaded files */
    final private static String UPLOAD_DIR = StandardGoogleApiVariables.UPLOAD_DIR;

    /** Define service account Email. */
    final private static String SERVICE_ACCOUNT_EMAIL = StandardGoogleApiVariables.GOOGLE_SERVICE_ACCOUNT_EMAIL;

    /** Define name of the file, that will be uploaded to Google Drive. */
    private static String uploadFileName;

    /** Define MIME type of the file */
    private static String uploadFileMimeType;

    /** Define global instance of HTTP_TRANSPORT */
    private static HttpTransport HTTP_TRANSPORT;

    /** Define global instance of JSON_FACTORY */
    private static JsonFactory JSON_FACTORY = new JacksonFactory();

    /** Define global Drive API Client */
    private static Drive drive;

    /** Define logger */
    final private static Logger logger = org.slf4j.LoggerFactory.getLogger(GoogleDriveUploader.class);

    // Constructors
    public GoogleDriveUploader() {}

    /**
     * Upload file from local drive to Google Drive.
     * <p/>
     * Use this method to send file via HTTP with an Google API to Google Drive.
     * <p/>
     * First of all, try to authorise at Google Drive service. After receiving all credentials, begin upload.
     * <p/>
     * In case, when file successful uploaded to GoogleDrive, try to set file sharing permissions to
     * "Share with anyone who has a link". If all that stuff completed success, return Map with info
     * about just uploaded file. Map contains Google Drive file ID and file link.
     *
     * @param fileName                          File name that should be uploaded to Google Drive.
     * @param mediaType                         MIME type of the file that should be uploaded to Google Drive.
     * @return                                  HashMap with uploaded file information: ID, link.
     * @throws IllegalArgumentException         If file name or media type are not present.
     * @throws GDriveUploadException            If upload to Google Drive fails for some reason.
     * @throws GDriveFilePermissionException    If setting file permissions on GDrive fails for some reason.
     * @throws GoogleTransportException         If can't pre-setup HTTP transport.
     * @throws GoogleAuthException              If can't authorize on service.
     */
    public static Map<String, String> uploadFileToDrive(String fileName, String mediaType) throws
            IllegalArgumentException, GDriveUploadException, GDriveFilePermissionException,
            GoogleTransportException, GoogleAuthException {
        logger.debug("uploadFileToDrive() - started.");

        if (fileName == null || fileName.isEmpty()) {
            logger.error("uploadFile() Can't upload file with empty name, please setup file name first.");
            logger.debug("uploadFile() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("File name can't be null or empty.");
        }

        if (mediaType == null || mediaType.isEmpty()) {
            logger.debug("uploadFile() Can't upload file with undefined MIME type. Please setup it firs.");
            logger.debug("uploadFile() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("File MIME type can't be undefined.");
        }

        logger.debug("uploadFileToDrive() Setting fileName to {} and fileType to {}", fileName, mediaType);
        setFileInfo(fileName, mediaType);

        logger.debug("uploadFileToDrive() Setting up HTTP_TRANSPORT.");
        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
            logger.info("uploadFileToDrive() HTTP_TRANSPORT success set up. {}", HTTP_TRANSPORT);
        } catch (Exception e) {
            logger.error("uploadFileToDrive() Can't set up HTTP_TRANSPORT. Got an Exception with error: {} " +
                    "and \nTrace:\n {}", e.getMessage(), Arrays.deepToString(e.getStackTrace()));
            logger.debug("uploadFileToDrive() finished with general GoogleTransportException");
            throw new GoogleTransportException("Can't configure HTTP Transport. Error: " + e.getMessage());
        }

        Credential credential = null;
        logger.debug("uploadFileToDrive() Start authorization.");
        try {
            credential = GoogleAuthorizer.getInstance()
                    //.setServiceScopes(Collections.singleton(DriveScopes.DRIVE))       // Define just drive scope.
                    .authorizeEndUser();
            logger.info("uploadFileToDrive() User authorized with credentials {}", credential);
        } catch (IOException e) {
            logger.error("uploadFileToDrive() Can't authorize user at Google. Got error message: {}, \nTrace:\n",
                    e.getMessage(), Arrays.deepToString(e.getStackTrace()));
            logger.debug("uploadFileToDrive() - finished with GoogleAuthException");
            throw new GoogleAuthException("Can't authorize. Error: " + e.getMessage());

        }

        drive = new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, null)
                .setHttpRequestInitializer(credential)
                .setApplicationName(APPLICATION_NAME)
                .build();
        logger.debug("Initiate drive {}", drive);

        File uploadedFile = null;
        logger.info("uploadFileToDrive() Trying to upload file to GDrive.");
        try {
            uploadedFile   = uploadFile(false);
            logger.info("uploadFileToDrive() Seams file was successful uploaded to GDrive. File ID: {}, File link: {}",
                    uploadedFile.getId(), uploadedFile.getAlternateLink());
        } catch (IOException e) {
            logger.error("uploadFileToDrive() Can't upload file to GDrive. IOException occurred with message: {} and" +
                    "\nTrace:\n {}", e.getMessage(), Arrays.deepToString(e.getStackTrace()));
            logger.debug("uploadFileToDrive() - finished with GDriveUploadException.");
            e.printStackTrace();
            throw new GDriveUploadException("Can't upload file to GDrive. Error: " + e.getMessage());
        }

        logger.info("uploadFileToDrive() Trying to set Permissions to file.");
        Map<String, String> fileInfo = new HashMap<>();

        if (uploadedFile != null) {
            Permission retrPermissions = null;
            try {
                retrPermissions = insertPermission(drive, uploadedFile.getId());
                logger.info("uploadFileToDrive() Permissions were success added. Added file Permissions {}", retrPermissions);
            } catch (IOException e) {
                logger.error("uploadFileToDrive() Can't add file permissions. Got IOException with message: {} and" +
                        "\nTrace:\n {}", e.getMessage(), Arrays.deepToString(e.getStackTrace()));
                logger.debug("uploadFileToDrive() - finished with GDriveFilePermissionsException.");
                throw new GDriveFilePermissionException("Can't set file permission. Error: " + e.getMessage());
            }

            fileInfo.put("id", uploadedFile.getId());
            fileInfo.put("alternateLink", uploadedFile.getAlternateLink());
            fileInfo.put("webViewLink", uploadedFile.getWebViewLink());
            fileInfo.put("webContentLink", uploadedFile.getWebContentLink());
        }

        logger.debug("Returning fileInfo: {}", fileInfo);

        logger.debug("uploadFileToDrive() - finished.");

        return fileInfo;
    }

    /**
     * Insert a new permission.
     * Enable file sharing to anyone who has a link.
     * <p/>
     * If you'd like to customise permissions, possible options listed bellow:<br/>
     * VALUE: User or group e-mail address, domain name or {@code null} "default" type.<br/>
     * TYPE: The value "user", "group", "domain" or "default".<br/>
     * ROLE: The value "owner", "writer" or "reader".<br/>
     *
     * @param service                   Drive API service instance.
     * @param fileId                    ID of the file to insert permission for.
     * @return                          The inserted permission if successful, {@code null} otherwise.
     * @throws IOException              Will thrown if permissions can't be set.
     */
    private static Permission insertPermission(Drive service, String fileId) throws IOException {
        logger.debug("insertPermission() - started.");
        Permission newPermission = new Permission();
        final String ROLE = "reader";
        final String TYPE = "anyone";
        final String ID = "anyoneWithLink";

        newPermission.setWithLink(true);
        newPermission.setValue(StandardGoogleApiVariables.GOOGLE_USER_EMAIL);
        newPermission.setType(TYPE);
        newPermission.setRole(ROLE);
        newPermission.setId(ID);

        try {
            logger.debug("Trying to add new permissions to file with ID {}", fileId);

            /*  Vict0r.
                ! Important !
                Please, do not remove setDisableGZipContent(true). Need to be here to Fix absence of setEnableGZipContent()
                in AbstractGoogleClientRequest.java. Was deprecated in 1.14 API.
             */
            Permission returnPermission = service.permissions().insert(fileId, newPermission)
                    .setDisableGZipContent(true)        // Vict0r: do not modify, until fix in AbstractGoogleClientRequest.
                    .execute();

            logger.debug("Permissions were added. Current permissions: {}", returnPermission);
            logger.debug("insertPermission() - finished.");
            return returnPermission;
        } catch (IOException e) {
            logger.error("insertPermission() - An error occurred while setting up permissions. Error message: {}, " +
                    "\nTrace:\n {}", e.getMessage(), Arrays.deepToString(e.getStackTrace()));
            logger.debug("insertPermission() - finished with an error. Please check file permissions by yourself.");
            throw new IOException(e);
        }
    }

    /**
     * Set upload file attributes in one place.
     *
     * @param fileName          Upload file name.
     * @param fileMime          Upload file MIME type.
     */
    private static void setFileInfo(String fileName, String fileMime) {
        uploadFileName = fileName;
        uploadFileMimeType = fileMime;
    }

    /**
     * File uploader.
     * <p/>
     * Upload file to Google Drive, when upload is finished, return uploaded file info.
     *
     * @param isDirectUpload            Set true if you want to use direct upload protocol.
     * @return                          Uploaded file info or null.
     * @throws IOException              If uploading file can't be read.
     */
    private static File uploadFile(boolean isDirectUpload) throws IOException {
        logger.debug("uploadFile() - started");

        String absFilePath = UPLOAD_DIR + uploadFileName;
        final java.io.File UPLOAD_FILE = new java.io.File(absFilePath);

        File fileMeta = new File();
        fileMeta.setTitle(UPLOAD_FILE.getName());
        FileContent mediaContent = new FileContent(uploadFileMimeType, UPLOAD_FILE);

        logger.debug("uploadFile() Uploading file {} with MIME type {} to GDrive.", fileMeta, mediaContent);

        Drive.Files.Insert insert = drive.files().insert(fileMeta, mediaContent);
        MediaHttpUploader uploader = insert.getMediaHttpUploader();
        uploader.setDirectUploadEnabled(isDirectUpload);
        uploader.setProgressListener(new FileUploadProgressListener());

        logger.debug("uploadFile() - finished.");
        return insert.execute();
    }
}
